# Copyright (C) 2001-2003, 2009-2010 The Written Word, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: misclib.py 803 2012-09-01 19:32:51Z china $

import os
import select
import signal
import stat
import string
import sys
import time
import urlparse
import warnings

from fnmatch import fnmatch

from xmlutil import etree, objectify
from version import Version

# paths
prefix = '%prefix%'
libdir = '%libdir%'
auxdir = libdir + '/aux'

# constants
DEFAULT_SYSTYPE_PATH = libdir + '/systype'

# verbosity levels
VERBOSE_LOCAL_DEPOT = 1		# if creating a temp local depot
VERBOSE_DEPOT = 2		# print depots being added
VERBOSE_DEBUG = 2		# run scripts through -x

_gnu_systype_cache = {}		# cache gnu_systype output
_gpg_path = None		# path to 'gpg' program

# exceptions
class MisclibError (Exception):
  """base class for all exceptions raised by this module"""

  def __init__ (self, arg = None):
    self.msg = arg

  def __str__ (self):
    return self.msg

class MisclibFileNotFoundError (MisclibError):
  """file not found"""

class MisclibGPGProgNotFoundError (MisclibError):
  """gpg program not found"""

class MisclibPackageNotFoundError (MisclibError):
  """package not found"""

class MisclibPkgDBNotFoundError (MisclibError):
  """package database directory not found"""

class MisclibTempdirCreationError (MisclibError):
  """error creating temp directory"""

class MisclibTempfileCreationError (MisclibError):
  """error creating temp file"""

class MisclibUnknownSystemError (MisclibError):
  """unknown system type"""

  def __init__ (self, arg):
    self.msg = 'unknown system type "' + arg + '"'

# return string containing entries in directory
def list_dir (path):
  entries = []

  files = os.listdir (path)
  files.sort ()
  for file in files:
    file_stat = os.lstat (os.path.join (path, file))

    # file size
    entry = string.rjust (str (file_stat[stat.ST_SIZE]), 10)

    # date
    entry += ' ' + time.strftime ('%b %d %H:%M', time.localtime \
    (file_stat[stat.ST_CTIME]))

    # name
    entry += ' ' + file

    entries.append (entry)

  return entries

# size of files in a directory
def dir_size (toplevel_dir):
  size = 0
  hard_link_inodes = []

  dirs = [toplevel_dir]
  while dirs:
    dir = dirs.pop (0)

    paths = os.listdir (dir)
    for path in paths:
      abs_path = os.path.join (dir, path)

      path_stat = os.lstat (abs_path)
      if stat.S_ISDIR (path_stat[stat.ST_MODE]):
        dirs.append (abs_path)
      elif stat.S_ISREG (path_stat[stat.ST_MODE]):
        if path_stat.st_nlink > 1:
          if path_stat.st_ino in hard_link_inodes:
            continue
          hard_link_inodes.append (path_stat.st_ino)

      size += path_stat[stat.ST_SIZE]

  return size

# remove directory. all files and subdirectories removed.
def rmdir (toplevel_dir):
  # make sure not removing directory while we are currently 
  # in the directory
  pwd = os.getcwd ()
  os.chdir (os.path.dirname (toplevel_dir))

  dirs = [toplevel_dir]
  for dir in dirs:
    files = os.listdir (dir)
    for file in files:
      filename = os.path.join (dir, file)
      if os.path.isfile (filename) or os.path.islink (filename):
        os.remove (filename)
      elif os.path.isdir (filename):
        dirs.append (filename)
  dirs.sort (lambda x, y: cmp (y, x))

  os.chdir (pwd)

  # remove directories by removing subdirectories first and
  # walking the directory tree backwards deleting directories
  # as we go
  for dir in dirs:
    os.rmdir (dir)

# create temp directory and make sure we own it
def mkstempdir (tmpbasedir = None):
  # Ignore runtime warning about tmpnam() being a potential
  # security risk. Our use does not cause a security risk.
  warnings.filterwarnings (action = 'ignore',
                           message = '.*potential security risk.*',
                           category = RuntimeWarning,
                           module = 'misclib')

  if tmpbasedir:
    # TMPDIR environment variable overrides tmpbasedir. Set TMPDIR
    # in case the package management utilities need temp files.
    os.environ['TMPDIR'] = tmpbasedir

    tmpdir = os.tempnam (tmpbasedir)
  else:
    tmpdir = os.tempnam ()

  try:
    os.mkdir (tmpdir)
  except (IOError, OSError), msg:
    raise MisclibTempdirCreationError ('unable to create temporary \
directory "' + tmpdir + '". error returned by mkdir() was "' +
    str (msg) + '".')

  # race condition where someone can introduce a symbolic link
  # between os.tempnam and os.mkdir so check
  if not stat.S_ISDIR (os.lstat (tmpdir)[stat.ST_MODE]):
    raise MisclibTempdirCreationError ('invalid temporary directory, "' +
    tmpdir + '". between calls to tempnam() and mkdir(), someone \
created a non-directory entry for "' + tmpdir + '". please fix and rerun.')

  return tmpdir

def mkstempfile (tmpbasedir = None):
  # Ignore runtime warning about tmpnam() being a potential
  # security risk. Our use does not cause a security risk.
  warnings.filterwarnings (action = 'ignore',
                           message = '.*potential security risk.*',
                           category = RuntimeWarning,
                           module = 'misclib')

  if tmpbasedir:
    tmpfile = os.tempnam (tmpbasedir)
  else:
    tmpfile = os.tempnam ()

  try:
    fd = os.open (tmpfile, os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_TRUNC, 0744)
    try:
      return os.fdopen (fd, 'w', -1), tmpfile
    except (IOError, OSError):
      os.close (fd)
      raise
  except (IOError, OSError), msg:
    raise MisclibTempfileCreationError (str (msg))

# find absolute path to program
def find_path_prog (prog, environ = None):
  if environ and environ.has_key ('PATH'):
    _path = environ['PATH']
  else:
    _path = os.environ['PATH']

  for path in _path.split (':'):
    abs_path = os.path.join (path, prog)

    if os.path.isfile (abs_path):
      return abs_path

  return None

def print_checking_status (package, output):
  if package.pkg_rev:
    output ('begin', 'checking if v' + package.prog_ver + '-' +
            package.pkg_rev + ' installed (' + package.pkgname_base + ')')
  else:
    output ('begin', 'checking if v' + package.prog_ver +
            ' installed (' + package.pkgname_base + ')')

# work around bugs on HP-UX where os.popen.readlines ()
class popen:
  def __init__ (self, path, args, mode):
    self.mode = mode

    # handle SIGCHLD from popen
    signal.signal (signal.SIGCHLD, self.__sigchld)

    self.popen_r, self.my_w = os.pipe ()
    self.my_r, self.popen_w = os.pipe ()
    self.popen = os.fork ()
    if self.popen == 0:
      os.close (self.my_r)
      os.close (self.my_w)
      os.dup2 (self.popen_r, 0)
      os.close (self.popen_r)
      os.dup2 (self.popen_w, 1)
      os.close (self.popen_w)

      os.execvp (path, [path] + args)

    os.close (self.popen_r)
    os.close (self.popen_w)
    self.io = select.poll ()
    self.fds = 1
    self.output = ''

    if mode == 'r':
      self.io.register (self.my_r, select.POLLIN)
      os.close (self.my_w)
    elif mode == 'w':
      self.io.register (self.my_w, select.POLLOUT)
      os.close (self.my_r)

  def readline (self):
    pass

  def readlines (self):
    output = []

    while self.fds:
      events = self.io.poll (0)

      for event in events:
        if event[0] == self.my_r:
          input_line = os.read (self.my_r, 1024)
          # separate output into newline-terminated strings
          self.output += input_line
          newline_pos = self.output.find ('\n')
          while newline_pos != -1:
            output.append (self.output[0:newline_pos + 1])

            self.output = self.output[newline_pos + 1:]
            newline_pos = self.output.find ('\n')

          if not input_line or event[1] == select.POLLHUP:
            self.io.unregister (self.my_r)
            os.close (self.my_r)
            self.fds -= 1

    self.close ()

    return output

  def close (self):
    # return to default SIGCHLD handler
    signal.signal (signal.SIGCHLD, signal.SIG_DFL)

  def __sigchld (self, signum, frame):
    os.waitpid (0, os.WNOHANG)

def verify_gpg_sig (file, sig, keyring_path):
  global _gpg_path

  # locate gpg program
  if not _gpg_path:
    _gpg_path = find_path_prog ('gpg')
    if not _gpg_path:
      raise MisclibGPGProgNotFoundError ('"gpg" binary not available \
in program search path $PATH')

  if os.fork () == 0:
    devnull = os.open ('/dev/null', os.O_WRONLY)
    os.dup2 (devnull, 1)
    os.dup2 (devnull, 2)

    sys.exit (os.spawnvp (os.P_WAIT, _gpg_path, [_gpg_path,
    '--no-default-keyring', '--batch', '--no-verbose', '--verify',
    '--homedir', keyring_path, sig, file]))
  else:
    pid, exit_status = os.wait ()
    if not exit_status:
      return 1

  return 0

# get GNU architecture type
def get_gnu_systype (systype_path = DEFAULT_SYSTYPE_PATH):
  global _gnu_systype_cache

  if _gnu_systype_cache.has_key (systype_path):
    return _gnu_systype_cache[systype_path]

  # Preserve custom SIGCHLD signal handler
  custom_sig = signal.getsignal (signal.SIGCHLD)

  signal.signal (signal.SIGCHLD, signal.SIG_DFL)
  gnu_systype = os.popen (systype_path).readline ().rstrip ()
  signal.signal (signal.SIGCHLD, custom_sig)

  _gnu_systype_cache[systype_path] = gnu_systype

  return gnu_systype

# match systype list against specific systype
def systype_match (systype_list, gnu_systype):
  for systype in systype_list.split ('|'):
    if systype[0] == '!':
      if not fnmatch (gnu_systype, systype[1:]):
        break
    else:
      if fnmatch (gnu_systype, systype):
        break
  else:
    return False

  return True

# determine default package type based on system type
def get_default_pkg_type (systype):
  if fnmatch (systype, '*-aix*'):
    pkg_type = 'lpp'
  elif fnmatch (systype, '*-hpux*'):
    pkg_type = 'depot'
  elif fnmatch (systype, '*-irix*'):
    pkg_type = 'inst'
  elif fnmatch (systype, '*-linux*'):
    pkg_type = 'rpm'
  elif fnmatch (systype, '*-osf*'):
    pkg_type = 'setld'
  elif fnmatch (systype, '*-solaris*'):
    pkg_type = 'pkgadd'

  return pkg_type

# When adding a file to a url directory, urlparse.urljoin requires
# a trailing slash:
def urljoin (path, file):
  if path.endswith ('/'):
    return urlparse.urljoin (path, file)
  else:
    return urlparse.urljoin (path + '/', file)
